<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>link rel=preload with various errors/non-errors</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/preload_helper.js"></script>
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self' http://{{hosts[alt][]}}:{{ports[http][0]}} 'unsafe-inline'">
<script>
// For various error/non-error network responses,, this test checks
// - load/error events fired on <link rel=preload>,
// - load/error events on main requests (e.g. <img>), and
// - preloads are reused for main requests
//   (by verifyLoadedAndNoDoubleDownload()).
//
// While this test expects <link rel=preload> error events only for network errors
// as specified in
// https://html.spec.whatwg.org/multipage/links.html#link-type-preload:fetch-and-process-the-linked-resource
// https://github.com/whatwg/html/pull/7799
// the actual browsers' behavior is different, and the feasibility of changing
// the behavior has not yet been investigated.
// https://github.com/whatwg/html/issues/1142.

setup({allow_uncaught_exception: true});

function preload(t, as, url, shouldPreloadSucceed) {
  return new Promise(resolve => {
    const link = document.createElement('link');
    link.setAttribute('rel', 'preload');
    link.setAttribute('as', as);
    link.setAttribute('crossorigin', 'anonymous');
    link.setAttribute('href', url);
    link.onload = t.step_func_done(() => {
      resolve();
      if (!shouldPreloadSucceed) {
        assert_unreached('preload onload');
      }
    });
    link.onerror = t.step_func_done(() => {
      resolve();
      if (shouldPreloadSucceed) {
        assert_unreached('preload onerror');
      }
    });
    document.head.appendChild(link);
  });
}

function runTest(api, as, description, shouldPreloadSucceed, shouldMainLoadSucceed,
                 urlWithoutLabel) {
  description += ' (' + api + ')';

  const url = new URL(urlWithoutLabel, location.href);
  url.searchParams.set('label', api);

  const tPreload = async_test(description + ': preload events');

  promise_test(async t => {
    let messageOnTimeout = 'timeout';
    t.step_timeout(() => t.unreached_func(messageOnTimeout)(), 3000);

    const preloadPromise = preload(tPreload, as, url, shouldPreloadSucceed);

    // The main request is started just after preloading is started and thus
    // HTTP response headers and errors are not observed yet.
    let mainPromise;
    if (api === 'image') {
      mainPromise = new Promise(t.step_func((resolve, reject) => {
          const img = document.createElement('img');
          img.setAttribute('crossorigin', 'anonymous');
          img.onload = resolve;
          img.onerror = () => reject(new TypeError('img onerror'));
          img.src = url;
          document.head.appendChild(img);
      }));
    } else if (api === 'style') {
      mainPromise = new Promise(t.step_func((resolve, reject) => {
          const link = document.createElement('link');
          link.setAttribute('rel', 'stylesheet');
          link.setAttribute('crossorigin', 'anonymous');
          link.onload = resolve;
          link.onerror = () => reject(new TypeError('link rel=stylesheet onerror'));
          link.href = url;
          document.head.appendChild(link);
      }));
    } else if (api === 'script') {
      mainPromise = new Promise(t.step_func((resolve, reject) => {
          const script = document.createElement('script');
          script.setAttribute('crossorigin', 'anonymous');
          script.onload = resolve;
          script.onerror = () => reject(new TypeError('script onerror'));
          script.src = url;
          document.head.appendChild(script);
      }));
    } else if (api === 'xhr') {
      mainPromise = new Promise(t.step_func((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open('GET', url, true);
          xhr.onload = resolve;
          xhr.onerror = () => reject(new TypeError('XHR onerror'));
          xhr.onabort = t.unreached_func('XHR onabort');
          xhr.send();
      }));
    } else if (api === 'fetch') {
      mainPromise = fetch(url)
        .then(r => {
          messageOnTimeout = 'fetch() completed but text() timeout';
          return r.text();
        });
    } else {
      throw new Error('Unexpected api: ' + api);
    }

    if (shouldMainLoadSucceed) {
      await mainPromise;
    } else {
      await promise_rejects_js(t, TypeError, mainPromise);
    }

    // Wait also for <link rel=preload> events.
    // This deflakes `verifyLoadedAndNoDoubleDownload` results.
    await preloadPromise;

    verifyLoadedAndNoDoubleDownload(url);
  }, description + ': main');
}

const tests = {
  'image': {
    url: '/preload/resources/square.png',
    as: 'image',
    mainLoadWillFailIf404Returned: false
  },
  'style': {
    url: '/preload/resources/dummy.css',
    as: 'style',

    // https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource
    mainLoadWillFailIf404Returned: true
  },
  'script': {
    url: '/preload/resources/dummy.js',
    as: 'script',

    // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
    mainLoadWillFailIf404Returned: true
  },
  'xhr': {
    url: '/preload/resources/dummy.xml',
    as: 'fetch',
    mainLoadWillFailIf404Returned: false
  },
  'fetch': {
    url: '/preload/resources/dummy.xml',
    as: 'fetch',
    mainLoadWillFailIf404Returned: false
  }
};

for (const api in tests) {
  const url = tests[api].url;
  const as = tests[api].as;

  // Successful response.
  runTest(api, as, 'success', true, true, url);

  // Successful response: non-ok status is not considered as a network error,
  // but can fire error events on main requests.
  runTest(api, as, '404', true, !tests[api].mainLoadWillFailIf404Returned,
      url + '?pipe=status(404)');

  // Successful response: Successful CORS check.
  runTest(api, as, 'CORS', true, true,
      'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url +
      '?pipe=header(Access-Control-Allow-Origin,*)');

  // A network error: Failed CORS check.
  runTest(api, as, 'CORS-error', false, false,
      'http://{{hosts[alt][]}}:{{ports[http][0]}}' + url);

  // A network error: Failed CSP check on redirect.
  runTest(api, as, 'CSP-error', false, false,
      '/common/redirect.py?location=http://{{hosts[alt][]}}:{{ports[http][1]}}' +
      url + '?pipe=header(Access-Control-Allow-Origin,*)');
}

// --------------------------------
// Content error.

// Successful response with corrupted image data.
// Not a network error, but can fire error events for images:
// https://html.spec.whatwg.org/multipage/images.html#update-the-image-data
runTest('image', 'image', 'Decode-error', true, false,
    '/preload/resources/dummy.css?pipe=header(Content-Type,image/png)');
runTest('style', 'style', 'Decode-error', true, true,
    '/preload/resources/dummy.xml?pipe=header(Content-Type,text/css)');
runTest('script', 'script', 'Decode-error', true, true,
    '/preload/resources/dummy.xml?pipe=header(Content-Type,text/javascript)');

// --------------------------------
// MIME Type error.
// Some MIME type mismatches are not network errors.
runTest('image', 'image', 'MIME-error', true, true,
    '/preload/resources/square.png?pipe=header(Content-Type,text/notimage)');
runTest('script', 'script', 'MIME-error', true, true,
    '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)');
// But they fire error events for <link rel=stylesheet>s.
// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource
runTest('style', 'style', 'MIME-error', true, false,
    '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)');

// Other MIME type mismatches are network errors, due to:
// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type?
runTest('script', 'script', 'MIME-blocked', false, false,
    '/preload/resources/dummy.css?pipe=header(Content-Type,image/not-javascript)');
// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
runTest('style', 'style', 'MIME-blocked-nosniff', false, false,
    '/preload/resources/dummy.js?pipe=header(Content-Type,not/css)|header(X-Content-Type-Options,nosniff)');
runTest('script', 'script', 'MIME-blocked-nosniff', false, false,
    '/preload/resources/dummy.css?pipe=header(Content-Type,text/notjavascript)|header(X-Content-Type-Options,nosniff)');
</script>
